Skip to content

Wave B-2: correctness — flush backoff, Flask dispose, exclude_hosts#37

Merged
AndresL230 merged 5 commits into
mainfrom
wave-b2/correctness
May 15, 2026
Merged

Wave B-2: correctness — flush backoff, Flask dispose, exclude_hosts#37
AndresL230 merged 5 commits into
mainfrom
wave-b2/correctness

Conversation

@AndresL230
Copy link
Copy Markdown
Contributor

@AndresL230 AndresL230 commented May 15, 2026

Summary

Closes #10
Closes #12
Closes #20

Three correctness bugs, no external dependencies.

#10 — Flush-loop exponential backoff

_timer_loop now tracks consecutive failures. After 5, it defers the next tick by min(1s * 2**(N-5), 5min) via the existing _deferral_ms cell (composes with 429-deferral via max). The backoff is announced via on_error exactly once per cap doubling, not once per tick. Counter resets on the next successful flush.

Side change: flush_and_send() now returns bool so the timer can distinguish a real successful send from a no-op empty-window flush — only a true send resets the failure streak (otherwise sparse traffic would prevent backoff from ever engaging).

#20 — Flask init_app() dispose-previous

RecostExtension.init_app() now disposes self._handle before reassigning. The same extension instance can bind to multiple apps without leaking a stale handle reference. Inherited by the deprecated ReCost alias.

#12exclude_hosts + *-rejection + loopback parity

  • New RecostConfig.exclude_hosts: List[str] field — exact-host match, distinct from exclude_patterns (substring).
  • init() raises ValueError for any exclude_patterns entry containing *. Error message points at exclude_hosts.
  • Cloud mode now derives base_url's host for auto-exclusion (not the full URL substring) — can't false-positive on URLs containing the base host as a query value.
  • Cloud mode pointed at a loopback (base_url=http://localhost:3000) now auto-excludes both localhost and 127.0.0.1 hosts.

Tests added

12 new tests across test_init.py and test_flask.py:

  • TestFlushLoopBackoff (4) — threshold, announcement, reset on success, bounded announcement count.
  • TestExcludeSemantics (6) — *-rejection, exact host match, cloud base_url host-only exclusion, loopback parity (cloud + local), local port substring.
  • TestInitAppDisposePrevious (2) — dispose-previous on rebind, idempotent on first call.

Local: 197 passed, mypy clean, ruff clean.

Test plan

  • CI green on Python 3.9, 3.10, 3.11, 3.12.
  • mypy recost/ clean.
  • ruff check recost/ tests/ clean.
  • README documents exclude_hosts vs exclude_patterns.

🤖 Generated with Claude Code

AndresL230 and others added 5 commits May 15, 2026 01:40
…12)

Distinct from exclude_patterns (substring). Wave B-2 step 1 of 3 — the
init() exclude logic and *-rejection ride in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… parity (#12)

- New: exact-host exclusion via exclude_hosts (short-circuits substring
  matching). Both fields apply additively.
- New: init() raises ValueError if any exclude_patterns entry contains
  '*' — substring is not glob; error points users at exclude_hosts.
- Fixed: cloud mode now derives base_url's HOST (not the full URL
  substring) for auto-exclusion. When base_url is a loopback,
  127.0.0.1 and localhost are both excluded so the substring quirk
  can't half-match.
- Fixed: local mode excludes both loopback hosts directly and uses a
  :PORT substring pattern for port-specific belt-and-suspenders.

Adds TestExcludeSemantics (6 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…es (#10)

Persistent flush errors (malformed metric, transport unreachable) used
to fire every flush_interval forever, generating CPU + log spam. After
5 consecutive failures, _timer_loop now defers the next tick by
min(1s * 2**(N-5), 5min) and announces the backoff via on_error —
exactly once per cap doubling, not once per tick. Counter resets on
the next successful flush.

Reuses the existing _deferral_ms cell so the 429 path and the
flush-backoff path compose via max() (longer wait wins).

flush_and_send() now returns bool so the timer can distinguish a real
successful send from a no-op empty-window flush — only a true send
resets the failure streak.

Adds TestFlushLoopBackoff (4 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the same RecostExtension instance binds to multiple apps (e.g.
in pytest fixtures or config-reload setups), init_app() previously
left self._handle pointing at the now-disposed handle from the prior
call. The fix adds a defensive dispose at the Flask layer so
self._handle is never stale. Idempotent — safe when self._handle
is already None.

Adds TestInitAppDisposePrevious (2 tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add exclude_hosts to configuration table and clarify the distinction:
exclude_patterns is substring match (no glob), exclude_hosts is exact host
match for unambiguous exclusion without false-positives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

The PR extends the Recost telemetry client with exact-host event exclusion, exponential backoff for repeated flush failures with operator visibility, improved flush return semantics, and Flask re-initialization safety. It addresses issue #10 (flush-loop error resilience) and adds corresponding test coverage for all new behaviors.

Changes

Event Exclusion, Backoff, and Flask Reinit

Layer / File(s) Summary
Configuration schema for host exclusion
recost/_types.py, README.md
Added exclude_hosts field to RecostConfig to support exact hostname matching distinct from substring exclude_patterns, with documentation clarifying how both are applied additively before aggregation.
Event exclusion logic and validation
recost/_init.py, tests/test_init.py
Validates exclude_patterns entries (rejecting glob *), builds exact-host exclusion set from exclude_hosts, base_url hostname, and loopback equivalence; early-returns events matching exact host before substring checks; comprehensive test suite covering patterns, hosts, and local/cloud loopback rules.
Flush return value for backoff tracking
recost/_init.py
Modified flush_and_send() to return bool (distinguishing no-op from actual sends) so backoff logic can track only consecutive successful flushes.
Exponential backoff with error reporting
recost/_init.py, tests/test_init.py
Timer loop now tracks consecutive flush failures and defers with exponential backoff after 5 failures; emits RecostError when backoff cap increases; resets failure streak only on successful sends; test suite validates threshold, reset, and cap-doubling behavior.
Flask extension disposal of prior handles
recost/frameworks/flask.py, tests/test_flask.py
init_app() now disposes any existing _handle before re-initialization, preventing orphaned timers; tests cover first-time and repeated initialization.

Sequence Diagram(s)

sequenceDiagram
  participant App as Flask App
  participant Ext as RecostExtension
  participant Handle as RecostHandle
  participant Timer as Timer Loop
  participant Agg as Aggregator
  participant Trans as Transport
  
  App->>Ext: init_app(first_app)
  Note over Ext: no prior handle
  Ext->>Handle: __init__()
  Handle->>Timer: start()
  
  App->>Ext: init_app(second_app)
  Note over Ext: prior handle exists
  Ext->>Handle: dispose()
  Note over Timer: stops, cleanup
  Ext->>Handle: __init__()
  Handle->>Timer: start()
  
  Timer->>Agg: flush()
  Agg->>Trans: send(summary)
  alt Success
    Trans-->>Timer: OK
    Note over Timer: reset consecutive_failures
  else Failure
    Trans-->>Timer: exception
    Note over Timer: consecutive_failures++
    alt consecutive_failures ≥ 5
      Timer->>Timer: exponential backoff
      Note over Timer: defer(backoff_ms)
      Note over Timer: emit RecostError
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 Events hop through filters fine,
Hosts excluded by design,
Failures backoff, don't retry blind—
Timers rest when loops unwind! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the three main changes: flush backoff, Flask dispose, and exclude_hosts functionality.
Linked Issues check ✅ Passed Code changes fully implement flush-loop backoff tracking, exponential deferral composition, on_error notifications, and failure-counter reset on successful flushes as specified in issue #10.
Out of Scope Changes check ✅ Passed All changes directly address requirements in the linked issue: backoff logic, Flask init_app dispose, and exclude_hosts + pattern validation are all scoped to the stated objectives.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-b2/correctness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@AndresL230 AndresL230 merged commit 55ed1f5 into main May 15, 2026
4 of 5 checks passed
@AndresL230 AndresL230 deleted the wave-b2/correctness branch May 21, 2026 04:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant